"""
Comprehensive tests for logging_utils/setup.py

Targets:
- USCentralFormatter.formatTime() with/without datefmt
- setup_server_logging() with various configurations
- restore_original_streams()
- Edge cases: OSError, None ws_manager, stdout/stderr redirection

Coverage target: 70-80% (40-50 statements out of 62 missing)
"""

import logging
import logging.handlers
import sys
from unittest.mock import MagicMock, patch

import pytest

from logging_utils.setup import (
    USCentralFormatter,
    restore_original_streams,
    setup_server_logging,
)

# ==================== FIXTURES ====================


@pytest.fixture(autouse=True)
def mock_rotating_file_handler():
    """Mock RotatingFileHandler globally to prevent file creation."""
    with patch("logging_utils.setup.logging.handlers.RotatingFileHandler") as mock:
        handler_instance = MagicMock()
        handler_instance.level = logging.NOTSET  # Fix for >= comparison
        mock.return_value = handler_instance
        yield mock


@pytest.fixture(autouse=True)
def mock_sys_stderr():
    """Mock sys.__stderr__ to prevent WinError 6 on Windows."""
    with patch("logging_utils.setup.sys.__stderr__") as mock:
        yield mock


@pytest.fixture
def mock_logger():
    """Create a fresh logger instance for each test."""
    logger = logging.getLogger("test_logger")
    logger.handlers.clear()
    logger.setLevel(logging.NOTSET)
    logger.propagate = False
    yield logger
    logger.handlers.clear()


@pytest.fixture
def mock_ws_manager():
    """Create a mock WebSocket connection manager."""
    manager = MagicMock()
    manager.broadcast = MagicMock()
    return manager


@pytest.fixture
def mock_dirs():
    """Mock directory creation."""
    with patch("logging_utils.setup.os.makedirs") as mock:
        yield mock


@pytest.fixture
def mock_file_ops():
    """Mock file operations (exists, remove)."""
    with (
        patch("logging_utils.setup.os.path.exists") as mock_exists,
        patch("logging_utils.setup.os.remove") as mock_remove,
    ):
        mock_exists.return_value = False
        yield mock_exists, mock_remove


# ==================== USCentralFormatter TESTS ====================


def test_uscentralformatter_without_datefmt():
    """Test USCentralFormatter with default datetime format."""
    formatter = USCentralFormatter()

    # Create a mock log record
    record = logging.LogRecord(
        name="test",
        level=logging.INFO,
        pathname="test.py",
        lineno=1,
        msg="Test message",
        args=(),
        exc_info=None,
    )
    record.created = 1732662000.123  # Known timestamp
    record.msecs = 123

    # Format the time
    formatted_time = formatter.formatTime(record)

    # Should return US/Central timezone formatted string
    assert formatted_time is not None
    assert "," in formatted_time  # Should include milliseconds
    assert "123" in formatted_time  # Milliseconds should be present


def test_uscentralformatter_with_custom_datefmt():
    """Test USCentralFormatter with custom datefmt."""
    formatter = USCentralFormatter()

    record = logging.LogRecord(
        name="test",
        level=logging.INFO,
        pathname="test.py",
        lineno=1,
        msg="Test message",
        args=(),
        exc_info=None,
    )
    record.created = 1732662000.123
    record.msecs = 123

    # Format with custom datefmt
    formatted_time = formatter.formatTime(record, datefmt="%Y/%m/%d %H:%M")

    # Should use custom format without milliseconds
    assert formatted_time is not None
    assert "/" in formatted_time
    assert "," not in formatted_time  # No milliseconds with custom format


def test_uscentralformatter_timezone():
    """Test that USCentralFormatter uses America/Chicago timezone."""
    formatter = USCentralFormatter()

    record = logging.LogRecord(
        name="test",
        level=logging.INFO,
        pathname="test.py",
        lineno=1,
        msg="Test message",
        args=(),
        exc_info=None,
    )
    # Use a known timestamp
    record.created = 1732662000.0  # 2024-11-26 ~18:00:00 UTC
    record.msecs = 0

    formatted_time = formatter.formatTime(record)

    # Verify it's a valid datetime string (format check)
    assert len(formatted_time) > 10
    assert "-" in formatted_time or ":" in formatted_time


# ==================== setup_server_logging BASIC TESTS ====================


def test_setup_server_logging_basic(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops
):
    """Test basic setup_server_logging functionality."""
    mock_exists, mock_remove = mock_file_ops

    orig_stdout, orig_stderr = setup_server_logging(
        logger_instance=mock_logger,
        log_ws_manager=mock_ws_manager,
        log_level_name="INFO",
        redirect_print_str="false",
    )

    # Verify directory creation was called
    assert mock_dirs.call_count == 3  # LOG_DIR, ACTIVE_AUTH_DIR, SAVED_AUTH_DIR

    # Verify logger configuration
    assert mock_logger.level == logging.INFO
    assert mock_logger.propagate is False

    # Verify handlers were added (file + console + websocket)
    assert len(mock_logger.handlers) == 3

    # Verify original streams were returned
    assert orig_stdout is not None
    assert orig_stderr is not None


def test_setup_server_logging_log_levels(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops
):
    """Test setup_server_logging with different log levels."""
    test_cases = [
        ("DEBUG", logging.DEBUG),
        ("INFO", logging.INFO),
        ("WARNING", logging.WARNING),
        ("ERROR", logging.ERROR),
        ("CRITICAL", logging.CRITICAL),
    ]

    for level_name, expected_level in test_cases:
        mock_logger.handlers.clear()

        setup_server_logging(
            logger_instance=mock_logger,
            log_ws_manager=mock_ws_manager,
            log_level_name=level_name,
            redirect_print_str="false",
        )

        assert mock_logger.level == expected_level, f"Failed for level {level_name}"


def test_setup_server_logging_invalid_log_level(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops
):
    """Test setup_server_logging with invalid log level defaults to INFO."""
    setup_server_logging(
        logger_instance=mock_logger,
        log_ws_manager=mock_ws_manager,
        log_level_name="INVALID_LEVEL",
        redirect_print_str="false",
    )

    # Should default to INFO
    assert mock_logger.level == logging.INFO


def test_setup_server_logging_clears_existing_handlers(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops
):
    """Test that setup_server_logging clears existing handlers."""
    # Add a dummy handler
    dummy_handler = logging.StreamHandler()
    mock_logger.addHandler(dummy_handler)
    assert len(mock_logger.handlers) == 1

    setup_server_logging(
        logger_instance=mock_logger,
        log_ws_manager=mock_ws_manager,
        log_level_name="INFO",
        redirect_print_str="false",
    )

    # Old handler should be removed, new handlers added
    assert dummy_handler not in mock_logger.handlers
    assert len(mock_logger.handlers) == 3


# ==================== HANDLER TESTS ====================


def test_setup_server_logging_file_handler(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops, mock_rotating_file_handler
):
    """Test that file handler is configured correctly."""
    setup_server_logging(
        logger_instance=mock_logger,
        log_ws_manager=mock_ws_manager,
        log_level_name="INFO",
        redirect_print_str="false",
    )

    # Verify RotatingFileHandler was created
    mock_rotating_file_handler.assert_called_once()
    call_kwargs = mock_rotating_file_handler.call_args[1]
    assert call_kwargs["maxBytes"] == 5 * 1024 * 1024  # 5 MB
    assert call_kwargs["backupCount"] == 5
    assert call_kwargs["encoding"] == "utf-8"
    assert call_kwargs["mode"] == "w"

    # Verify formatter was set
    mock_rotating_file_handler.return_value.setFormatter.assert_called_once()


def test_setup_server_logging_console_handler(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops
):
    """Test that console handler is configured correctly."""
    setup_server_logging(
        logger_instance=mock_logger,
        log_ws_manager=mock_ws_manager,
        log_level_name="WARNING",
        redirect_print_str="false",
    )

    # Find console handler (StreamHandler writing to stderr)
    console_handlers = [
        h for h in mock_logger.handlers if isinstance(h, logging.StreamHandler)
    ]
    assert len(console_handlers) > 0

    # Verify console handler level matches logger level
    console_handler = console_handlers[0]
    assert console_handler.level == logging.WARNING


def test_setup_server_logging_websocket_handler_valid(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops
):
    """Test WebSocket handler is added when manager is provided."""
    with patch("logging_utils.setup.WebSocketLogHandler") as MockWSHandler:
        mock_ws_handler = MagicMock()
        mock_ws_handler.level = logging.NOTSET  # Fix for >= comparison
        MockWSHandler.return_value = mock_ws_handler

        setup_server_logging(
            logger_instance=mock_logger,
            log_ws_manager=mock_ws_manager,
            log_level_name="INFO",
            redirect_print_str="false",
        )

        # Verify WebSocket handler was created and configured
        MockWSHandler.assert_called_once_with(mock_ws_manager)
        mock_ws_handler.setLevel.assert_called_once_with(logging.INFO)


def test_setup_server_logging_websocket_handler_none(
    mock_logger, mock_dirs, mock_file_ops, capsys, mock_sys_stderr
):
    """Test behavior when WebSocket manager is None."""
    with patch("logging_utils.setup.WebSocketLogHandler") as MockWSHandler:
        setup_server_logging(
            logger_instance=mock_logger,
            log_ws_manager=None,
            log_level_name="INFO",
            redirect_print_str="false",
        )

        # WebSocketLogHandler should NOT be created
        MockWSHandler.assert_not_called()

        # Should print warning to stderr (mocked)
        # Verify any of the write calls contains the warning
        calls = mock_sys_stderr.write.call_args_list
        warning_found = any(
            "严重警告" in str(call.args[0]) or "log_ws_manager" in str(call.args[0])
            for call in calls
        )
        assert warning_found, f"Warning not found in sys.__stderr__ writes: {calls}"


# ==================== FILE OPERATIONS TESTS ====================


def test_setup_server_logging_removes_existing_log_file(
    mock_logger, mock_ws_manager, mock_dirs
):
    """Test that existing log file is removed."""
    with (
        patch("logging_utils.setup.os.path.exists") as mock_exists,
        patch("logging_utils.setup.os.remove") as mock_remove,
    ):
        mock_exists.return_value = True

        setup_server_logging(
            logger_instance=mock_logger,
            log_ws_manager=mock_ws_manager,
            log_level_name="INFO",
            redirect_print_str="false",
        )

        # Verify file removal was attempted
        mock_remove.assert_called_once()


def test_setup_server_logging_oserror_on_remove(
    mock_logger, mock_ws_manager, mock_dirs, capsys, mock_sys_stderr
):
    """Test handling of OSError when removing log file."""
    with (
        patch("logging_utils.setup.os.path.exists") as mock_exists,
        patch("logging_utils.setup.os.remove") as mock_remove,
    ):
        mock_exists.return_value = True
        mock_remove.side_effect = OSError("Permission denied")

        # Should not raise exception
        setup_server_logging(
            logger_instance=mock_logger,
            log_ws_manager=mock_ws_manager,
            log_level_name="INFO",
            redirect_print_str="false",
        )

        # Should print warning to stderr (mocked)
        calls = mock_sys_stderr.write.call_args_list
        warning_found = any(
            "警告" in str(call.args[0]) or "Permission denied" in str(call.args[0])
            for call in calls
        )
        assert warning_found, f"Warning not found in sys.__stderr__ writes: {calls}"


# ==================== STDOUT/STDERR REDIRECTION TESTS ====================


def test_setup_server_logging_redirect_print_false(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops
):
    """Test that stdout/stderr are NOT redirected when redirect_print_str is 'false'."""
    original_stdout = sys.stdout
    original_stderr = sys.stderr

    orig_stdout, orig_stderr = setup_server_logging(
        logger_instance=mock_logger,
        log_ws_manager=mock_ws_manager,
        log_level_name="INFO",
        redirect_print_str="false",
    )

    # Stdout/stderr should remain unchanged
    assert sys.stdout == original_stdout
    assert sys.stderr == original_stderr

    # Returned originals should match current
    assert orig_stdout == original_stdout
    assert orig_stderr == original_stderr


def test_setup_server_logging_redirect_print_true(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops
):
    """Test that stdout/stderr ARE redirected when redirect_print_str is 'true'."""
    original_stdout = sys.stdout
    original_stderr = sys.stderr

    with patch("logging_utils.setup.StreamToLogger") as MockStreamToLogger:
        mock_stream = MagicMock()
        MockStreamToLogger.return_value = mock_stream

        orig_stdout, orig_stderr = setup_server_logging(
            logger_instance=mock_logger,
            log_ws_manager=mock_ws_manager,
            log_level_name="INFO",
            redirect_print_str="true",
        )

        # Verify StreamToLogger was created for both stdout and stderr
        assert MockStreamToLogger.call_count == 2

        # Returned originals should be the original streams
        assert orig_stdout == original_stdout
        assert orig_stderr == original_stderr


def test_setup_server_logging_redirect_print_variations(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops
):
    """Test redirect_print with various truthy values."""
    test_cases = [
        ("true", True),
        ("True", True),
        ("1", True),
        ("yes", True),
        ("false", False),
        ("False", False),
        ("0", False),
        ("no", False),
        ("", False),
    ]

    for input_val, should_redirect in test_cases:
        with patch("logging_utils.setup.StreamToLogger") as MockStreamToLogger:
            mock_stream = MagicMock()
            MockStreamToLogger.return_value = mock_stream

            setup_server_logging(
                logger_instance=mock_logger,
                log_ws_manager=mock_ws_manager,
                log_level_name="INFO",
                redirect_print_str=input_val,
            )

            if should_redirect:
                assert MockStreamToLogger.called, f"Failed for input: {input_val}"
            else:
                assert not MockStreamToLogger.called, f"Failed for input: {input_val}"


# ==================== THIRD-PARTY LOGGER TESTS ====================


def test_setup_server_logging_configures_third_party_loggers(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops
):
    """Test that third-party library loggers are configured."""
    with patch("logging_utils.setup.logging.getLogger") as mock_get_logger:
        # Create mock loggers for third-party libraries
        third_party_loggers = {}
        for name in [
            "uvicorn",
            "uvicorn.error",
            "uvicorn.access",
            "websockets",
            "playwright",
            "asyncio",
        ]:
            third_party_loggers[name] = MagicMock()

        def get_logger_side_effect(name):
            if name in third_party_loggers:
                return third_party_loggers[name]
            return MagicMock()

        mock_get_logger.side_effect = get_logger_side_effect

        setup_server_logging(
            logger_instance=mock_logger,
            log_ws_manager=mock_ws_manager,
            log_level_name="INFO",
            redirect_print_str="false",
        )

        # Verify third-party loggers were configured
        # Note: We can't easily verify the setLevel calls due to the patching,
        # but we can verify getLogger was called for them
        assert mock_get_logger.call_count >= 6


# ==================== restore_original_streams TESTS ====================


def test_restore_original_streams(capsys, mock_sys_stderr):
    """Test restore_original_streams restores stdout and stderr."""
    original_stdout = sys.stdout
    original_stderr = sys.stderr

    # Replace with mock streams
    mock_stdout = MagicMock()
    mock_stderr = MagicMock()
    sys.stdout = mock_stdout
    sys.stderr = mock_stderr

    # Restore
    restore_original_streams(original_stdout, original_stderr)

    # Verify restoration
    assert sys.stdout == original_stdout
    assert sys.stderr == original_stderr

    # Note: restore_original_streams is now silent (no confirmation message)
    # This is intentional to reduce shutdown log noise


def test_restore_original_streams_with_streamtologger():
    """Test restore_original_streams when streams are StreamToLogger instances."""
    original_stdout = sys.stdout
    original_stderr = sys.stderr

    # Mock StreamToLogger
    mock_stream_stdout = MagicMock()
    mock_stream_stderr = MagicMock()
    sys.stdout = mock_stream_stdout
    sys.stderr = mock_stream_stderr

    restore_original_streams(original_stdout, original_stderr)

    # Verify restoration
    assert sys.stdout == original_stdout
    assert sys.stderr == original_stderr


# ==================== INTEGRATION TESTS ====================


def test_setup_and_restore_full_cycle(
    mock_logger, mock_ws_manager, mock_dirs, mock_file_ops
):
    """Test full setup -> log -> restore cycle."""
    original_stdout = sys.stdout
    original_stderr = sys.stderr

    # Setup
    orig_streams = setup_server_logging(
        logger_instance=mock_logger,
        log_ws_manager=mock_ws_manager,
        log_level_name="DEBUG",
        redirect_print_str="false",
    )

    # Verify setup
    assert len(mock_logger.handlers) == 3
    assert mock_logger.level == logging.DEBUG

    # Test logging
    mock_logger.info("Test message")
    mock_logger.debug("Debug message")
    mock_logger.error("Error message")

    # Restore
    restore_original_streams(*orig_streams)

    # Verify restoration
    assert sys.stdout == original_stdout
    assert sys.stderr == original_stderr


def test_setup_server_logging_creates_directories(mock_ws_manager, mock_file_ops):
    """Test that all required directories are created."""
    with patch("logging_utils.setup.os.makedirs") as mock_makedirs:
        logger = logging.getLogger("test_dirs")
        logger.handlers.clear()

        setup_server_logging(
            logger_instance=logger,
            log_ws_manager=mock_ws_manager,
            log_level_name="INFO",
            redirect_print_str="false",
        )

        # Verify makedirs was called 3 times (LOG_DIR, ACTIVE_AUTH_DIR, SAVED_AUTH_DIR)
        assert mock_makedirs.call_count == 3

        # Verify exist_ok=True was used
        for call_args in mock_makedirs.call_args_list:
            assert call_args[1]["exist_ok"] is True

        logger.handlers.clear()
